#!/usr/bin/python
from Tkinter import *
import datetime


class Configuration:
	def __init__(self, filename = 'config.txt'):

		self.opt = {}
		file_handle = open(filename, 'r')

		for line in file_handle:
			if line[0] == '#' or line == '\n':
				continue
			line = line.replace(':', ' ')
			try:
				(key, value) = line.split()
			except ValueError:
				items = line.split()
				key = items[0]
				value = ' '.join(items[1:])
			try:
				if float(value) == int(value):
					self.opt[key] = int(value)
				else:
					self.opt[key] = float(value)
			except:
				self.opt[key] = value
		
		self.label_font = '-*-' + self.opt['label-font-family'] + '-Normal-R-*--*-' + str(self.opt['label-font-size'] * 10) + '-*-*-*-*-*'
		self.entry_font = '-*-' + self.opt['entry-font-family'] + '-Normal-R-*--*-' + str(self.opt['entry-font-size'] * 10) + '-*-*-*-*-*'
		self.listbox_font = '-*-' + self.opt['listbox-font-family'] + '-Normal-R-*--*-'
		self.listbox_font += str(self.opt['listbox-font-size'] * 10) + '-*-*-*-*-*'
		
		file_handle.close()


			
class Application (Frame):
	def __init__(self, parent):
		Frame.__init__(self, parent)
		self.group = Group(self)
		self.group.buffer_ = Buffer_(self.group)
		self.group.items = Items(self.group)
		
		self.status = StringVar()
		self.point_value = StringVar()
		self.axislabels = StringVar()
		
		self.cfg = Configuration()
		if self.cfg.opt['log'] == 'True':
			self.log_initialise()
				
		self.grid()
		
		self.populate()
		self.layout()
		self.bindings()
		self.create_menu()
		
		self.axes.focus_set()
		
		self.scalebar.set(self.cfg.opt['initial-scale'])
		self.axislabels.set('axis-1 axis-2 axis-3')
		self.previous_scale = self.scalebar.get()
		self.refresh_axes()
		self.log('Application loaded')
		
		
	def populate (self):
		""" Create widgets """
	
		# For the sake of brevity
		opt = self.cfg.opt
		
		self.canvas = Canvas(self, width = opt['canvas-width'], height = opt['canvas-height'], takefocus = 0)
		self.panel = Frame(self, takefocus = 0)
		self.statuslabel = Label(self, textvariable = self.status, font = self.cfg.label_font, takefocus = 0)
		self.object_name_label = Label(self, font = self.cfg.label_font, takefocus = 0)
		self.scalebar = Scale(self, from_ = opt['scale-min'], takefocus = 0, to = opt['scale-max'], length = opt['canvas-height'], command = self.scale_changed)

		
		self.reset = Button(self, text = 'Reset', takefocus = 0, command = self.reset)
		self.retire = Button(self, text = 'Quit', takefocus = 0, command = self.retire_now)
		
		self.point = Entry(self.panel, textvariable = self.point_value, font = self.cfg.entry_font, takefocus = 1)
		self.axes = Entry(self, textvariable = self.axislabels, font = self.cfg.entry_font, takefocus = 1)

		self.axes_change = Button(self, text = 'Change', takefocus = 0, command = self.refresh_axes)
		self.add = Button(self.panel, text = 'Add', takefocus = 0, command = self.group.buffer_.add)

		self.buffer_lb = Listbox(self.panel, takefocus = 1, font = self.cfg.listbox_font)
		self.object_name = Entry(self.panel, font = self.cfg.entry_font, takefocus = 1)
		self.cancel = Button(self.panel, text = 'Cancel', takefocus = 0, command = self.group.buffer_.cancel)
		self.plot = Button(self.panel, text = 'Plot', takefocus = 0, command = self.group.buffer_.plot_handler)
	
		self.highlight = Button(self.panel, text = 'Highlight', takefocus = 0, command = self.group.items.highlight)
		self.remove = Button(self.panel, text = 'Remove', takefocus = 0, command = self.group.items.remove)
		

		self.items_lb = Listbox(self.panel, takefocus = 1, font = self.cfg.listbox_font)


	def layout (self):
		""" Assemble widgets """
	
		# Grid within the whole window
		self.statuslabel.grid(row = 0, column = 0, columnspan = 2, sticky = W)
		self.axes.grid(row = 0, column = 2, sticky = W)
		self.axes_change.grid(row = 0, column = 2, sticky = E)
		self.canvas.grid(row = 1, column = 0, columnspan = 2, rowspan = 2)
		self.panel.grid(row = 1, column = 2)
		self.scalebar.grid(row = 0, column = 3, rowspan = 2)
		self.reset.grid(row = 2, column = 2, sticky = W)
		self.retire.grid(row = 2, column = 2, sticky = E)
		self.object_name_label.grid(row = 2, column = 0, columnspan = 2)
		
		# Grid within the panel for the list boxes
		self.point.grid(row = 0, column = 0)
		self.add.grid(row = 0, column = 1)
		self.buffer_lb.grid(row = 1, column = 0, columnspan = 2)
		self.plot.grid(row = 2, column = 0)
		self.cancel.grid(row = 2, column = 1)
		self.object_name.grid(row = 4, column = 0, columnspan = 2)
		self.items_lb.grid(row = 6, column = 0, columnspan = 2)
		self.highlight.grid(row = 7, column = 0)
		self.remove.grid(row = 8, column = 1)
	
	
	def bindings (self):
		""" Keyboard bindings """
	
		self.axes.bind('<Return>', self.refresh_axes)
		self.point.bind('<Return>', self.group.buffer_.add)
		self.object_name.bind('<Return>', self.keyboard_plot_callback)
		
		root.bind('<Control-q>', self.retire_now)
		root.bind('<Control-p>', self.keyboard_plot_callback)
		root.bind('<Control-a>', self.group.buffer_.add)
		
		self.buffer_lb.bind('<BackSpace>', self.group.buffer_.cancel)
		self.items_lb.bind('<Return>', self.group.items.highlight)
		self.items_lb.bind('<BackSpace>', self.group.items.remove)
	
	
	def keyboard_plot_callback (self, event = None):
		self.group.buffer_.plot_handler()
	
	def create_menu (self):
		menubar = Menu(root)

		# create a pulldown menu, and add it to the menu bar
		command_menu = Menu(menubar, tearoff = 0)
		command_menu.add_command(label = "Add to buffer", command = self.group.buffer_.add)
		command_menu.add_command(label = "Plot from buffer", command = self.group.buffer_.plot_handler)
		command_menu.add_command(label = 'Highlight object', command = self.group.items.highlight)
		command_menu.add_separator()
		command_menu.add_command(label = "Quit (Ctrl+Q)", command = self.retire_now)
		menubar.add_cascade(label = "Commands", menu = command_menu)
		
		root.config(menu = menubar)
	
	def retire_now (self, event = None):
		""" End program (quit and exit are both reserved keywords!) """
		
		self.log_file_handle.close()
		root.destroy()
	
	# Methods for refreshing screen	
		
	def scale_changed (self, new_value):
		""" Called by scale scrollbar; organises events that must happen then """
	
		initial_scale = self.cfg.opt['initial-scale']
		diff = abs(self.previous_scale - int(new_value))
		
		self.scalebar.set(new_value)
		self.group.buffer_.wipe()
		self.refresh_canvas()

		if diff > self.cfg.opt['scale-change-delta']:
			self.log('Scale changed to ' + str(float(new_value) / initial_scale) + 'x')
			self.previous_scale = int(new_value)

		
	def reset (self):
		""" Wipes canvas, redraws axes, and wipes buffer """
		
		self.canvas.delete(ALL)
		self.refresh_axes()
		self.group.buffer_.wipe()
		
		self.group.items.objects = {}
		self.group.items.object_pts = {}
	
	def refresh_axes (self, event = None):
		""" Wipes canvas and draws the axis lines on the canvas """
		
		self.canvas.delete('axis')
		
		self.group.buffer_.plot_handler(origin = 'axes')
		self.group.buffer_.plot_axis_labels()

		
	def refresh_canvas (self):
		""" Wipes canvas and then redraws axis lines, labels, and items """
		
		self.canvas.delete(ALL)
		self.refresh_axes()
		self.group.buffer_.plot_handler(origin = 'refresh_objects')

	# Logging methods

	def log_initialise (self):
		""" Initialise log file session """
	
		filename = self.cfg.opt['log-file']
		self.log_file_handle = open(filename, 'a')
		
		log_time = datetime.datetime.now().strftime('%x %X')
		log_line = '\n' + log_time + ' Log file initialised\n'
		self.log_file_handle.write(log_line)

	def log (self, account):
		""" Sends account to log file and status label on GUI """
		
		self.status.set(account)
		
		if self.cfg.opt['log'] == 'False':
			return
		log_time = datetime.datetime.now().strftime('%x %X')
		log_line = log_time + ' ' + account + '\n'
		
		self.log_file_handle.write(log_line)



class Group:
	def __init__ (self, parent):
		self.p = parent



class Buffer_:
	def __init__ (self, parent):
		self.p = parent
		self.points = {}
	
	# Buffer_ class methods called by the interface

	def add (self, event = None):
		""" Add point in parent's entry field to the Buffer_ class dictionary """
		
		root.focus_set()

		point = self.p.p.point_value.get()
		new_point = self.str_to_list(point)
		n = 1
		while True:
			if not self.points.has_key(n):
				self.points[n] = new_point
				break
			else:
				n += 1
		self.p.p.log('Added ' + str(n) + ': ' + ', '.join(map(str, new_point)) + ' to buffer')
		self.refresh()

	def cancel (self, event = None):
		""" Remove selected point (of parent's buffer_lb) and refresh """

		string = self.p.p.buffer_lb.get(ACTIVE)
		point = self.str_to_list(string)
		i = point[0]
		del self.points[i]

		self.p.p.log('Removed ' + string + ' from buffer')
		self.refresh()

	def plot_handler (self, origin = '', points = [], event = None):
		""" Handler for the plot function """
		
		if event:
			print 'Keyboard reached plot_handler'
		
		if origin == '':
			# Arrived from GUI; need to plot the buffer 
			self.plot()
			
		elif origin == 'axes':
			# Plot the axes without going near the objects{} or the buffer_
			triangle = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
			xy = map(self.tern_to_xy, triangle)
			self.p.p.canvas.create_polygon(fill = '', outline = 'black', tags = 'axis', *xy)
			return
		
		elif origin == 'refresh_objects':
			# Just re-plot the objects{} with no reference to the buffer_
			self.plot_objects()

	def plot (self, origin = '', points = []):
		""" Draw the contents of Buffer_ dictionary on the parent's canvas """

		number = len(self.points)
		xy = []
		tern = self.points.values()
		
		if number == 1:
			tern = self.points.values()[0]
			xy = self.tern_to_xy(tern)
			self.i = self.plot_point_low_level(xy)
			txt = ', '.join(map(str, self.points.values()[0]))
			self.p.p.log('Plotted point at ' + txt)
			
		elif number == 2:
			xy = map(self.tern_to_xy, tern)
			self.i = self.p.p.canvas.create_line(*xy)
			txt = ''
			for point in self.points.values():
				txt += '(' + ', '.join(map(str, point)) + ')'
				txt += ', '
			self.p.p.log('Plotted line between ' + txt)

		elif number > 2:
			xy = map(self.tern_to_xy, tern)
			self.i = self.p.p.canvas.create_polygon(fill = '', outline = 'black', *xy)
			txt = ''
			for point in self.points.values():
				txt += '(' + ', '.join(map(str, point)) + ')'
				txt += ', '
			self.p.p.log('Plotted polygon between ' + txt)

		self.p.items.add(number)
		
		if self.p.p.cfg.opt['empty-buffer-after-plot'] == 'True':
			self.wipe()

	def plot_objects (self, origin = '', points = []):
		""" Draw the contents of items.object_pts{}
		By necessity this routine also wipes item.object_pts and item.objects and re-makes
		them, as the canvas item numbers (may) have changed. """

		new_object_pts = self.p.items.object_pts.copy()
		new_objects = self.p.items.objects.copy()
		
		self.p.items.object_pts = {}
		self.p.items.objects = {}

		for old_i, name in new_objects.items():
			points = new_object_pts[old_i]
			
			number = len(points)
			xy = map(self.tern_to_xy, points)			

			if number == 1:
				new_i = self.plot_point_low_level(*xy)
			elif number == 2:
				new_i = self.p.p.canvas.create_line(*xy)
			elif number == 3:
				new_i = self.p.p.canvas.create_polygon(fill = '', outline = 'black', *xy)
			
			self.p.items.objects[new_i] = name
			self.p.items.object_pts[new_i] = points
			
	
	def plot_axis_labels (self):
		""" Plot axis labels """
		
		tern = [([1.02, -0.025, -0.025], NE), ([-0.025, 1.02, -0.025], NW), ([-0.025, -0.025, 1.02], S)]
		labels = self.p.p.axislabels.get().split()
		label_font = '-*-Verdana-Normal-R-*--*-140-*-*-*-*-ISO8859-1'
		
		for i in [0, 1, 2]:
			xy = self.tern_to_xy(tern[i][0])
			pos = tern[i][1]
			self.p.p.canvas.create_text(text = labels[i], anchor = pos, tags = 'axis', font = label_font, *xy)
		

			
	# Internal Buffer_ class methods

	def refresh (self):
		""" Refresh parent's buffer_lb so it reflects Buffer_'s dictionary """

		self.p.p.buffer_lb.delete(0, END)
		for each in self.points.items():
			i, point = each
			string = str(i) + ': (' + ', '.join(map(str, point)) + ')'
			self.p.p.buffer_lb.insert(END, string)
	
	def wipe (self):
		""" Empty Buffer_ dictionary and parent's buffer_lb """
		
		self.points = {}
		self.refresh()

	def tern_to_xy (self, tern):
		""" Convert ternary/triangular coordinates to Cartesian ones suitable for parent.canvas """

		a, b, c = tern
		scale = self.p.p.scalebar.get()
		initial_scale = self.p.p.cfg.opt['initial-scale']
		x, y = 0, 0
		
		x = b + c / 2.0
		y = c * 0.8660254
		
		x *= scale
		y *= scale
		
		x += scale / 10
		y = ((0.866 * scale) - y) + scale / 10
		
		if scale < initial_scale:
			x += (initial_scale - scale) / 1.5
		return x, y
	
	def plot_point_low_level (self, xy):
		""" Display a point by a circle of a certain radius """
		
		x = xy[0]
		y = xy[1]
		circle_radius = self.p.p.cfg.opt['point-radius']
		
		top_right = [x - circle_radius, y + circle_radius]
		bottom_left = [x + circle_radius, y - circle_radius]
		
		return self.p.p.canvas.create_oval(top_right + bottom_left)
	
	def str_to_list (self, string):
		""" Turn a delimited string of numbers into a list of floats """
		
		delimiters = [',', '/', ':', ';', '~', '|', '(', ')']
		for each in delimiters:
			string = string.replace(each, ' ')
		try:
			return map(float, string.split())
		except:
			self.p.p.log('Error: not a valid list of numbers')



class Items:
	def __init__ (self, parent):
		self.p = parent
		self.objects = {}
		self.object_pts = {}
	
	
	def add (self, number = -1):
		
		i = self.p.buffer_.i
		points = self.p.buffer_.points.values()
		name = self.p.p.object_name.get()
		if invert(self.objects).has_key(name):
			self.p.p.log('Error: duplicate name. Please use unique label for each point')
			name += '-1'
		if name == '':
			name = 'unnamed'

		self.objects[i] = name
		self.object_pts[i] = points
		
		self.refresh()

	def show (self):
		pass


	def hide (self):
		pass


	def highlight (self, event = None):
		
		for i in self.p.p.canvas.find_withtag('highlighted'):
			self.p.p.canvas.itemconfig(i, tags = '', fill = '')
		
		name = self.p.p.items_lb.get(ACTIVE)
		i = invert(self.objects)[name]
		
		self.p.p.canvas.itemconfig(i, fill = self.p.p.cfg.opt['fill-colour'], tags = 'highlighted')
		
		self.p.p.log('Highlighted item ' + name)


	def remove (self, event = None):
		""" Remove items_lb selection from listbox, canvas, and items.objects dictionary """
		
		name = self.p.p.items_lb.get(ACTIVE)
		i = invert(self.objects)[name]
		
		self.p.p.items_lb.delete(ACTIVE)
		self.p.p.canvas.delete(i)
		
		del self.objects[i]
		
		self.refresh()
	
	def refresh (self):
		""" Refresh parent's items_lb so it reflects Items' dictionary """

		self.p.p.items_lb.delete(0, END)
		for name in self.objects.values():
			self.p.p.items_lb.insert(END, name)
		


def invert (dictionary) :
	return dict([[v, k] for k, v in dictionary.items()])


# Initialise application
root = Tk()
root.title('Pternary 0.2.1')

# Try to hide annoying console window that appears on OS X
try:
	root.tk.call('console', 'hide')
except:
	pass

# Create an instance of our Application
app = Application(root)

# Hand things over to Tkinter
app.mainloop()

